baubs git
Commit 8b6eb593d9a8b7ab5f26c6107c946c5562dc1851
Parents : e1a64e9
Author : Jan-Henrik Bruhn <jan-henrik.bruhn@offis.de>
Date : 2025-12-21T00:16:52+01:00
feature: Add shadcn Tooltips to AppHeader for better UX
- Migrated serial number tooltip from native title to shadcn Tooltip
- Shows formatted machine details (serial, MAC, total stitches, service count)
- Better multi-line formatting with proper spacing
- Added tooltip to machine status badge
- Shows state description explaining current status
- Added tooltip to auto-refresh spinner
- Shows "Auto-refreshing machine status"
- Removed redundant title attributes
- Disconnect button already has clear label
- Error button has Popover for details
Benefits:
- Consistent tooltip styling across the app
- Better accessibility with ARIA attributes
- Improved readability with proper formatting
- Better positioning and animations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes
Diff
diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx
index 41b9036..2f1fe20 100644
--- a/src/components/AppHeader.tsx
+++ b/src/components/AppHeader.tsx
@@ -15,6 +15,12 @@ import {
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Popover, PopoverTrigger } from "@/components/ui/popover";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
export function AppHeader() {
@@ -61,145 +67,171 @@ export function AppHeader() {
const StatusIcon = stateIcons[stateVisual.iconName];
return (
- <header className="bg-gradient-to-r from-primary-600 via-primary-700 to-primary-800 dark:from-primary-700 dark:via-primary-800 dark:to-primary-900 px-4 sm:px-6 lg:px-8 py-3 shadow-lg border-b-2 border-primary-900/20 dark:border-primary-800/30 flex-shrink-0">
- <div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-4 lg:gap-8 items-center">
- {/* Machine Connection Status - Responsive width column */}
- <div className="flex items-center gap-3 w-full lg:w-[280px]">
- <div
- className="w-2.5 h-2.5 bg-success-400 rounded-full animate-pulse shadow-lg shadow-success-400/50"
- style={{ visibility: isConnected ? "visible" : "hidden" }}
- ></div>
- <div
- className="w-2.5 h-2.5 bg-gray-400 rounded-full -ml-2.5"
- style={{ visibility: !isConnected ? "visible" : "hidden" }}
- ></div>
- <div className="flex-1 min-w-0">
- <div className="flex items-center gap-2">
- <h1 className="text-lg lg:text-xl font-bold text-white leading-tight">
- Respira
- </h1>
- {isConnected && machineInfo?.serialNumber && (
- <span
- className="text-xs text-primary-200 cursor-help"
- title={`Serial: ${machineInfo.serialNumber}${
- machineInfo.macAddress
- ? `\nMAC: ${machineInfo.macAddress}`
- : ""
- }${
- machineInfo.totalCount !== undefined
- ? `\nTotal stitches: ${machineInfo.totalCount.toLocaleString()}`
- : ""
- }${
- machineInfo.serviceCount !== undefined
- ? `\nStitches since service: ${machineInfo.serviceCount.toLocaleString()}`
- : ""
- }`}
- >
- • {machineInfo.serialNumber}
- </span>
- )}
- {isPolling && (
- <ArrowPathIcon
- className="w-3.5 h-3.5 text-primary-200 animate-spin"
- title="Auto-refreshing status"
- />
- )}
- </div>
- <div className="flex items-center gap-2 mt-1 min-h-[32px]">
- {isConnected ? (
- <>
- <Button
- onClick={disconnect}
- size="sm"
- variant="outline"
- className="gap-1.5 bg-white/10 hover:bg-danger-600 text-primary-100 hover:text-white border-white/20 hover:border-danger-600 flex-shrink-0"
- title="Disconnect from machine"
- aria-label="Disconnect from machine"
- >
- <XMarkIcon className="w-3 h-3" />
- Disconnect
- </Button>
- <Badge
- variant="outline"
- className="gap-1.5 px-2.5 py-1.5 sm:py-1 text-sm font-semibold bg-white/20 text-white border-white/30 flex-shrink-0"
- >
- <StatusIcon className="w-3 h-3" />
- {machineStatusName}
- </Badge>
- </>
- ) : (
- <p className="text-xs text-primary-200">Not Connected</p>
- )}
+ <TooltipProvider>
+ <header className="bg-gradient-to-r from-primary-600 via-primary-700 to-primary-800 dark:from-primary-700 dark:via-primary-800 dark:to-primary-900 px-4 sm:px-6 lg:px-8 py-3 shadow-lg border-b-2 border-primary-900/20 dark:border-primary-800/30 flex-shrink-0">
+ <div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-4 lg:gap-8 items-center">
+ {/* Machine Connection Status - Responsive width column */}
+ <div className="flex items-center gap-3 w-full lg:w-[280px]">
+ <div
+ className="w-2.5 h-2.5 bg-success-400 rounded-full animate-pulse shadow-lg shadow-success-400/50"
+ style={{ visibility: isConnected ? "visible" : "hidden" }}
+ ></div>
+ <div
+ className="w-2.5 h-2.5 bg-gray-400 rounded-full -ml-2.5"
+ style={{ visibility: !isConnected ? "visible" : "hidden" }}
+ ></div>
+ <div className="flex-1 min-w-0">
+ <div className="flex items-center gap-2">
+ <h1 className="text-lg lg:text-xl font-bold text-white leading-tight">
+ Respira
+ </h1>
+ {isConnected && machineInfo?.serialNumber && (
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <span className="text-xs text-primary-200 cursor-help">
+ • {machineInfo.serialNumber}
+ </span>
+ </TooltipTrigger>
+ <TooltipContent className="max-w-xs">
+ <div className="text-sm space-y-1">
+ <p className="font-semibold">
+ Serial: {machineInfo.serialNumber}
+ </p>
+ {machineInfo.macAddress && (
+ <p className="text-xs">
+ MAC: {machineInfo.macAddress}
+ </p>
+ )}
+ {machineInfo.totalCount !== undefined && (
+ <p className="text-xs">
+ Total stitches:{" "}
+ {machineInfo.totalCount.toLocaleString()}
+ </p>
+ )}
+ {machineInfo.serviceCount !== undefined && (
+ <p className="text-xs">
+ Stitches since service:{" "}
+ {machineInfo.serviceCount.toLocaleString()}
+ </p>
+ )}
+ </div>
+ </TooltipContent>
+ </Tooltip>
+ )}
+ {isPolling && (
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <div>
+ <ArrowPathIcon className="w-3.5 h-3.5 text-primary-200 animate-spin" />
+ </div>
+ </TooltipTrigger>
+ <TooltipContent>
+ <p className="text-xs">Auto-refreshing machine status</p>
+ </TooltipContent>
+ </Tooltip>
+ )}
+ </div>
+ <div className="flex items-center gap-2 mt-1 min-h-[32px]">
+ {isConnected ? (
+ <>
+ <Button
+ onClick={disconnect}
+ size="sm"
+ variant="outline"
+ className="gap-1.5 bg-white/10 hover:bg-danger-600 text-primary-100 hover:text-white border-white/20 hover:border-danger-600 flex-shrink-0"
+ aria-label="Disconnect from machine"
+ >
+ <XMarkIcon className="w-3 h-3" />
+ Disconnect
+ </Button>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Badge
+ variant="outline"
+ className="gap-1.5 px-2.5 py-1.5 sm:py-1 text-sm font-semibold bg-white/20 text-white border-white/30 flex-shrink-0 cursor-help"
+ >
+ <StatusIcon className="w-3 h-3" />
+ {machineStatusName}
+ </Badge>
+ </TooltipTrigger>
+ <TooltipContent>
+ <p className="text-xs">{stateVisual.description}</p>
+ </TooltipContent>
+ </Tooltip>
+ </>
+ ) : (
+ <p className="text-xs text-primary-200">Not Connected</p>
+ )}
- {/* Error indicator - always render to prevent layout shift */}
- <Popover>
- <PopoverTrigger asChild>
- <Button
- size="sm"
- variant="destructive"
- className={cn(
- "gap-1.5 flex-shrink-0",
- machineErrorMessage || pyodideError
- ? "animate-pulse hover:animate-none"
- : "invisible pointer-events-none",
- )}
- title="Click to view error details"
- aria-label="View error details"
- disabled={!(machineErrorMessage || pyodideError)}
- >
- <ExclamationTriangleIcon className="w-3.5 h-3.5 flex-shrink-0" />
- <span>
- {(() => {
- if (pyodideError) return "Python Error";
- if (isPairingError) return "Pairing Required";
+ {/* Error indicator - always render to prevent layout shift */}
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button
+ size="sm"
+ variant="destructive"
+ className={cn(
+ "gap-1.5 flex-shrink-0",
+ machineErrorMessage || pyodideError
+ ? "animate-pulse hover:animate-none"
+ : "invisible pointer-events-none",
+ )}
+ aria-label="View error details"
+ disabled={!(machineErrorMessage || pyodideError)}
+ >
+ <ExclamationTriangleIcon className="w-3.5 h-3.5 flex-shrink-0" />
+ <span>
+ {(() => {
+ if (pyodideError) return "Python Error";
+ if (isPairingError) return "Pairing Required";
- const errorMsg = machineErrorMessage || "";
+ const errorMsg = machineErrorMessage || "";
- // Categorize by error message content
- if (
- errorMsg.toLowerCase().includes("bluetooth") ||
- errorMsg.toLowerCase().includes("connection")
- ) {
- return "Connection Error";
- }
- if (errorMsg.toLowerCase().includes("upload")) {
- return "Upload Error";
- }
- if (errorMsg.toLowerCase().includes("pattern")) {
- return "Pattern Error";
- }
- if (machineError !== undefined) {
- return `Machine Error`;
- }
+ // Categorize by error message content
+ if (
+ errorMsg.toLowerCase().includes("bluetooth") ||
+ errorMsg.toLowerCase().includes("connection")
+ ) {
+ return "Connection Error";
+ }
+ if (errorMsg.toLowerCase().includes("upload")) {
+ return "Upload Error";
+ }
+ if (errorMsg.toLowerCase().includes("pattern")) {
+ return "Pattern Error";
+ }
+ if (machineError !== undefined) {
+ return `Machine Error`;
+ }
- // Default fallback
- return "Error";
- })()}
- </span>
- </Button>
- </PopoverTrigger>
+ // Default fallback
+ return "Error";
+ })()}
+ </span>
+ </Button>
+ </PopoverTrigger>
- {/* Error popover content */}
- {(machineErrorMessage || pyodideError) && (
- <ErrorPopoverContent
- machineError={
- machineError != 0xdd ? machineError : undefined
- }
- isPairingError={isPairingError}
- errorMessage={machineErrorMessage}
- pyodideError={pyodideError}
- />
- )}
- </Popover>
+ {/* Error popover content */}
+ {(machineErrorMessage || pyodideError) && (
+ <ErrorPopoverContent
+ machineError={
+ machineError != 0xdd ? machineError : undefined
+ }
+ isPairingError={isPairingError}
+ errorMessage={machineErrorMessage}
+ pyodideError={pyodideError}
+ />
+ )}
+ </Popover>
+ </div>
</div>
</div>
- </div>
- {/* Workflow Stepper - Flexible width column */}
- <div>
- <WorkflowStepper />
+ {/* Workflow Stepper - Flexible width column */}
+ <div>
+ <WorkflowStepper />
+ </div>
</div>
- </div>
- </header>
+ </header>
+ </TooltipProvider>
);
}
Served by rngit 1.3.3 - Generated in 0.08s